// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package mapper

import (
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/prometheus/statsd_exporter/pkg/mappercache/lru"
	"github.com/prometheus/statsd_exporter/pkg/mappercache/randomreplacement"
)

type mappings []struct {
	statsdMetric string
	name         string
	labels       map[string]string
	quantiles    []MetricObjective
	notPresent   bool
	ttl          time.Duration
	metricType   MetricType
	maxAge       time.Duration
	ageBuckets   uint32
	bufCap       uint32
	buckets      []float64
	scale        MaybeFloat64
}

func newTestMapperWithCache(cacheType string, size int) *MetricMapper {
	mapper := MetricMapper{}
	var cache MetricMapperCache
	switch cacheType {
	case "lru":
		cache, _ = lru.NewMetricMapperLRUCache(mapper.Registerer, size)
	case "random":
		cache, _ = randomreplacement.NewMetricMapperRRCache(mapper.Registerer, size)
	case "none":
		return &mapper
	}
	mapper.UseCache(cache)
	return &mapper
}

func TestMetricMapperYAML(t *testing.T) {
	scenarios := []struct {
		testName  string
		config    string
		configBad bool
		mappings  mappings
	}{
		{
			testName: "Empty config",
		},
		{
			testName: "Config with several mapping definitions",
			config: `---
mappings:
- match: test.dispatcher.*.*.*
  name: "dispatch_events"
  labels:
    processor: "$1"
    action: "$2"
    result: "$3"
    job: "test_dispatcher"
- match: test.my-dispatch-host01.name.dispatcher.*.*.*
  name: "host_dispatch_events"
  labels:
    processor: "$1"
    action: "$2"
    result: "$3"
    job: "test_dispatcher"
- match: request_time.*.*.*.*.*.*.*.*.*.*.*.*
  name: "tyk_http_request"
  labels:
    method_and_path: "${1}"
    response_code: "${2}"
    apikey: "${3}"
    apiversion: "${4}"
    apiname: "${5}"
    apiid: "${6}"
    ipv4_t1: "${7}"
    ipv4_t2: "${8}"
    ipv4_t3: "${9}"
    ipv4_t4: "${10}"
    orgid: "${11}"
    oauthid: "${12}"
- match: "*.*"
  name: "catchall"
  labels:
    first: "$1"
    second: "$2"
    third: "$3"
    job: "$1-$2-$3"
- match: (.*)\.(.*)-(.*)\.(.*)
  match_type: regex
  name: "proxy_requests_total"
  labels:
    job: "$1"
    protocol: "$2"
    endpoint: "$3"
    result: "$4"

  `,
			mappings: mappings{
				{
					statsdMetric: "test.dispatcher.FooProcessor.send.succeeded",
					name:         "dispatch_events",
					labels: map[string]string{
						"processor": "FooProcessor",
						"action":    "send",
						"result":    "succeeded",
						"job":       "test_dispatcher",
					},
				},
				{
					statsdMetric: "test.my-dispatch-host01.name.dispatcher.FooProcessor.send.succeeded",
					name:         "host_dispatch_events",
					labels: map[string]string{
						"processor": "FooProcessor",
						"action":    "send",
						"result":    "succeeded",
						"job":       "test_dispatcher",
					},
				},
				{
					statsdMetric: "request_time.get/threads/1/posts.200.00000000.nonversioned.discussions.a11bbcdf0ac64ec243658dc64b7100fb.172.20.0.1.12ba97b7eaa1a50001000001.",
					name:         "tyk_http_request",
					labels: map[string]string{
						"method_and_path": "get/threads/1/posts",
						"response_code":   "200",
						"apikey":          "00000000",
						"apiversion":      "nonversioned",
						"apiname":         "discussions",
						"apiid":           "a11bbcdf0ac64ec243658dc64b7100fb",
						"ipv4_t1":         "172",
						"ipv4_t2":         "20",
						"ipv4_t3":         "0",
						"ipv4_t4":         "1",
						"orgid":           "12ba97b7eaa1a50001000001",
						"oauthid":         "",
					},
				},
				{
					statsdMetric: "foo.bar",
					name:         "catchall",
					labels: map[string]string{
						"first":  "foo",
						"second": "bar",
						"third":  "",
						"job":    "foo-bar-",
					},
				},
				{
					statsdMetric: "foo.bar.baz",
				},
				{
					statsdMetric: "proxy-1.http-goober.success",
					name:         "proxy_requests_total",
					labels: map[string]string{
						"job":      "proxy-1",
						"protocol": "http",
						"endpoint": "goober",
						"result":   "success",
					},
				},
			},
		},
		{
			testName: "Config with backtracking",
			config: `
defaults:
  glob_disable_ordering: true
mappings:
- match: backtrack.*.bbb
  name: "testb"
  labels:
    label: "${1}_foo"
- match: backtrack.justatest.aaa
  name: "testa"
  labels:
    label: "${1}_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "backtrack.good.bbb",
					name:         "testb",
					labels: map[string]string{
						"label": "good_foo",
					},
				},
				{
					statsdMetric: "backtrack.justatest.bbb",
					name:         "testb",
					labels: map[string]string{
						"label": "justatest_foo",
					},
				},
				{
					statsdMetric: "backtrack.justatest.aaa",
					name:         "testa",
					labels: map[string]string{
						"label": "_foo",
					},
				},
			},
		},
		// Config with backtracking, the non-matched rule has star(s)
		// A metric like full.name.anothertest will first match full.name.* and then tries
		// to match *.dummy.* and then failed.
		// This test case makes sure the captures in the non-matched later rule
		// doesn't affect the captures in the first matched rule.
		{
			testName: "Config with backtracking, the non-matched rule has star(s)",
			config: `
defaults:
  glob_disable_ordering: false
mappings:
- match: '*.dummy.*'
  name: metric_one
  labels:
    system: $1
    attribute: $2
- match: 'full.name.*'
  name: metric_two
  labels:
    system: static
    attribute: $1
`,
			mappings: mappings{
				{
					statsdMetric: "whatever.dummy.test",
					name:         "metric_one",
					labels: map[string]string{
						"system":    "whatever",
						"attribute": "test",
					},
				},
				{
					statsdMetric: "full.name.anothertest",
					name:         "metric_two",
					labels: map[string]string{
						"system":    "static",
						"attribute": "anothertest",
					},
				},
			},
		},
		{
			testName: "Config with super sets, disables ordering",
			config: `
defaults:
  glob_disable_ordering: true
mappings:
- match: noorder.*.*
  name: "testa"
  labels:
    label: "${1}_foo"
- match: noorder.*.bbb
  name: "testb"
  labels:
    label: "${1}_foo"
- match: noorder.ccc.bbb
  name: "testc"
  labels:
    label: "ccc_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "noorder.good.bbb",
					name:         "testb",
					labels: map[string]string{
						"label": "good_foo",
					},
				},
				{
					statsdMetric: "noorder.ccc.bbb",
					name:         "testc",
					labels: map[string]string{
						"label": "ccc_foo",
					},
				},
			},
		},
		{
			testName: "Config with super sets, keeps ordering",
			config: `
defaults:
  glob_disable_ordering: false
mappings:
- match: order.*.*
  name: "testa"
  labels:
    label: "${1}_foo"
- match: order.*.bbb
  name: "testb"
  labels:
    label: "${1}_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "order.good.bbb",
					name:         "testa",
					labels: map[string]string{
						"label": "good_foo",
					},
				},
			},
		},
		{
			testName: "Config with bad regex reference",
			config: `---
mappings:
- match: test.*
  name: "name"
  labels:
    label: "$1_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "test.a",
					name:         "name",
					labels: map[string]string{
						"label": "",
					},
				},
			},
		},
		{
			testName: "Config with good regex reference",
			config: `
mappings:
- match: test.*
  name: "name"
  labels:
    label: "${1}_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "test.a",
					name:         "name",
					labels: map[string]string{
						"label": "a_foo",
					},
				},
			},
		},
		{
			testName: "Match metric segment with a number",
			config: `
mappings:
- match: test.99.myapp.*
  name: "name"
  labels:
    label: "${1}_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "test.99.myapp.create",
					name:         "name",
					labels: map[string]string{
						"label": "create_foo",
					},
				},
			},
		},
		{
			testName: "Match metric segment starting with a number",
			config: `
mappings:
- match: test.99test.myapp.*
  name: "name"
  labels:
    label: "${1}_foo"
  `,
			mappings: mappings{
				{
					statsdMetric: "test.99test.myapp.create",
					name:         "name",
					labels: map[string]string{
						"label": "create_foo",
					},
				},
			},
		},
		{
			testName: "Match metric segment with a number #328 example",
			config: `
mappings:
- match: kafka.server.FetcherStats.brokerHost.hostname.brokerPort.9092.clientId.ReplicaFetcherThread-0-1.BytesPerSec.1MinuteRate.gauge
  name: "example_name"
  `,
			mappings: mappings{
				{
					statsdMetric: "kafka.server.FetcherStats.brokerHost.hostname.brokerPort.9092.clientId.ReplicaFetcherThread-0-1.BytesPerSec.1MinuteRate.gauge",
					name:         "example_name",
				},
			},
		},
		{
			testName: "Single segment match",
			config: `
mappings:
- match: '*'
  name: single_segment
  labels:
    label: "${1}"
`,
			mappings: mappings{
				{
					statsdMetric: "test",
					name:         "single_segment",
					labels: map[string]string{
						"label": "test",
					},
				},
			},
		},
		{
			testName: "Config with bad metric line",
			config: `---
mappings:
- match: bad!!metric-line.*.*
  name: "foo"
  labels: {}
  `,
			configBad: true,
		},
		{
			testName: "Config with multiple dashes in metric name",
			config: `---
mappings:
- match: "foo--bar.*"
  name: "foo_bar_${1}"
  labels: {}
  `,
			mappings: mappings{
				{
					statsdMetric: "foo--bar.count",
					name:         "foo_bar_count",
				},
			},
		},
		{
			testName: "Config with dynamic metric name",
			config: `---
mappings:
- match: test1.*.*
  name: "$1"
  labels: {}
- match: test2.*.*
  name: "${1}_$2"
  labels: {}
- match: test3\.(\w+)\.(\w+)
  match_type: regex
  name: "${2}_$1"
  labels: {}
  `,
			mappings: mappings{
				{
					statsdMetric: "test1.total_requests.count",
					name:         "total_requests",
				},
				{
					statsdMetric: "test2.total_requests.count",
					name:         "total_requests_count",
				},
				{
					statsdMetric: "test3.total_requests.count",
					name:         "count_total_requests",
				},
			},
		},
		{
			testName: "Config with bad metric name",
			config: `---
mappings:
- match: test.*.*
  name: "0foo"
  labels: {}
  `,
			configBad: true,
		},
		{
			testName: "Config with no metric name",
			config: `---
mappings:
- match: test.*.*
  labels:
    this: "$1"
  `,
			configBad: true,
		},
		{
			testName: "Config with no mappings",
			config:   ``,
			mappings: mappings{},
		},
		{
			testName: "Config without a trailing newline",
			config: `mappings:
- match: test.*
  name: "name"
  labels:
    label: "${1}_foo"`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
					name:         "name",
					labels: map[string]string{
						"label": "a_foo",
					},
				},
			},
		},
		{
			testName: "Config with an improperly escaped *",
			config: `
mappings:
- match: *.test.*
  name: "name"
  labels:
    label: "${1}_foo"`,
			configBad: true,
		},
		{
			testName: "Config with a properly escaped *",
			config: `
mappings:
- match: "*.test.*"
  name: "name"
  labels:
    label: "${2}_foo"`,
			mappings: mappings{
				{
					statsdMetric: "foo.test.a",
					name:         "name",
					labels: map[string]string{
						"label": "a_foo",
					},
				},
			},
		},
		{
			testName: "Config with good observer type",
			config: `---
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  quantiles:
    - quantile: 0.42
      error: 0.04
    - quantile: 0.7
      error: 0.002
  `,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
				},
			},
		},
		{
			testName: "Config with good observer type and unused timer type",
			config: `---
mappings:
- match: test.*.*
  observer_type: summary
  timer_type: histogram
  name: "foo"
  labels: {}
  quantiles:
    - quantile: 0.42
      error: 0.04
    - quantile: 0.7
      error: 0.002
  `,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
				},
			},
		},
		{
			testName: "Config with good observertype and no defaults",
			config: `---
mappings:
- match: test1.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  `,
			mappings: mappings{
				{
					statsdMetric: "test1.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.5, Error: 0.05},
						{Quantile: 0.9, Error: 0.01},
						{Quantile: 0.99, Error: 0.001},
					},
				},
			},
		},
		{
			testName: "Config with good deprecated timer type",
			config: `---
mappings:
- match: test1.*.*
  timer_type: summary
  name: "foo"
  labels: {}
  `,
			mappings: mappings{
				{
					statsdMetric: "test1.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.5, Error: 0.05},
						{Quantile: 0.9, Error: 0.01},
						{Quantile: 0.99, Error: 0.001},
					},
				},
			},
		},
		{
			testName: "Config with bad observer type",
			config: `---
mappings:
- match: test.*.*
  observer_type: wrong
  name: "foo"
  labels: {}
    `,
			configBad: true,
		},
		{
			testName: "Config with bad deprecated timer type",
			config: `---
mappings:
- match: test.*.*
  timer_type: wrong
  name: "foo"
  labels: {}
    `,
			configBad: true,
		},
		{
			testName: "New style quantiles",
			config: `---
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  summary_options:
    quantiles:
      - quantile: 0.42
        error: 0.04
      - quantile: 0.7
        error: 0.002
  `,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
				},
			},
		},
		{
			testName: "Config with summary options",
			config: `---
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  summary_options:
    quantiles:
      - quantile: 0.42
        error: 0.04
      - quantile: 0.7
        error: 0.002
    max_age: 5m
    age_buckets: 2
    buf_cap: 1000
  `,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
					maxAge:     5 * time.Minute,
					ageBuckets: 2,
					bufCap:     1000,
				},
			},
		},
		{
			testName: "Config with default summary options",
			config: `---
defaults:
 summary_options:
   quantiles:
     - quantile: 0.42
       error: 0.04
     - quantile: 0.7
       error: 0.002
   max_age: 5m
   age_buckets: 2
   buf_cap: 1000
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
					maxAge:     5 * time.Minute,
					ageBuckets: 2,
					bufCap:     1000,
				},
			},
		},
		{
			testName: "Config with default summary options without quantiles",
			config: `---
defaults:
 summary_options:
   max_age: 5m
   age_buckets: 2
   buf_cap: 1000
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles:    defaultQuantiles,
					maxAge:       5 * time.Minute,
					ageBuckets:   2,
					bufCap:       1000,
				},
			},
		},
		{
			testName: "Config with default summary options overrides quantiles",
			config: `---
defaults:
  quantiles:
    - quantile: 0.9
      error: 0.1
    - quantile: 0.99
      error: 0.01
  summary_options:
    quantiles:
      - quantile: 0.42
        error: 0.04
      - quantile: 0.7
        error: 0.002
    max_age: 5m
    age_buckets: 2
    buf_cap: 1000
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
					maxAge:     5 * time.Minute,
					ageBuckets: 2,
					bufCap:     1000,
				},
			},
		},
		{
			testName: "Config that overrides default summary options",
			config: `---
defaults:
 summary_options:
   quantiles:
     - quantile: 0.042
       error: 0.4
     - quantile: 0.07
       error: 0.02
   max_age: 15m
   age_buckets: 3
   buf_cap: 100
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  summary_options:
    quantiles:
     - quantile: 0.42
       error: 0.04
     - quantile: 0.7
       error: 0.002
    max_age: 5m
    age_buckets: 2
    buf_cap: 1000
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
					maxAge:     5 * time.Minute,
					ageBuckets: 2,
					bufCap:     1000,
				},
			},
		},
		{
			testName: "Config that overrides default summary options and a default options mapping",
			config: `---
defaults:
 summary_options:
   quantiles:
     - quantile: 0.9
       error: 0.1
     - quantile: 0.99
       error: 0.01
   max_age: 15m
   age_buckets: 3
   buf_cap: 100
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  summary_options:
    quantiles:
     - quantile: 0.42
       error: 0.04
     - quantile: 0.7
       error: 0.002
    max_age: 5m
    age_buckets: 2
    buf_cap: 1000
- match: test_default.*.*
  observer_type: summary
  name: "foo_default"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
					maxAge:     5 * time.Minute,
					ageBuckets: 2,
					bufCap:     1000,
				},
				{
					statsdMetric: "test_default.*.*",
					name:         "foo_default",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.9, Error: 0.1},
						{Quantile: 0.99, Error: 0.01},
					},
					maxAge:     15 * time.Minute,
					ageBuckets: 3,
					bufCap:     100,
				},
			},
		},
		{
			testName: "Config that partially overrides default summary quantiles and a default options mapping",
			config: `---
defaults:
 summary_options:
   quantiles:
     - quantile: 0.9
       error: 0.1
     - quantile: 0.99
       error: 0.01
   max_age: 15m
   age_buckets: 3
   buf_cap: 100
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  summary_options:
    quantiles:
     - quantile: 0.42
       error: 0.04
     - quantile: 0.7
       error: 0.002
- match: test_default.*.*
  observer_type: summary
  name: "foo_default"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.42, Error: 0.04},
						{Quantile: 0.7, Error: 0.002},
					},
					maxAge:     15 * time.Minute,
					ageBuckets: 3,
					bufCap:     100,
				},
				{
					statsdMetric: "test_default.*.*",
					name:         "foo_default",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.9, Error: 0.1},
						{Quantile: 0.99, Error: 0.01},
					},
					maxAge:     15 * time.Minute,
					ageBuckets: 3,
					bufCap:     100,
				},
			},
		},
		{
			testName: "Config that partially overrides default summary max_age and a default options mapping",
			config: `---
defaults:
 summary_options:
   quantiles:
     - quantile: 0.9
       error: 0.1
     - quantile: 0.99
       error: 0.01
   max_age: 15m
   age_buckets: 3
   buf_cap: 100
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  summary_options:
    max_age: 5m
- match: test_default.*.*
  observer_type: summary
  name: "foo_default"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.9, Error: 0.1},
						{Quantile: 0.99, Error: 0.01},
					},
					maxAge:     5 * time.Minute,
					ageBuckets: 3,
					bufCap:     100,
				},
				{
					statsdMetric: "test_default.*.*",
					name:         "foo_default",
					labels:       map[string]string{},
					quantiles: []MetricObjective{
						{Quantile: 0.9, Error: 0.1},
						{Quantile: 0.99, Error: 0.01},
					},
					maxAge:     15 * time.Minute,
					ageBuckets: 3,
					bufCap:     100,
				},
			},
		},
		{
			testName: "Config with histogram options",
			config: `---
mappings:
- match: test.*.*
  observer_type: histogram
  name: "foo"
  labels: {}
  histogram_options:
    buckets: [0.1, 1, 10, 100, 1000]
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					buckets:      []float64{0.1, 1, 10, 100, 1000},
				},
			},
		},
		{
			testName: "Config with default histogram options",
			config: `---
defaults:
  histogram_options:
    buckets: [0.1, 1, 10, 100, 1000]
mappings:
- match: test.*.*
  observer_type: histogram
  name: "foo"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					buckets:      []float64{0.1, 1, 10, 100, 1000},
				},
			},
		},
		{
			testName: "Config with default histogram options without buckets",
			config: `---
defaults:
  histogram_options:
    buckets: []
mappings:
- match: test.*.*
  observer_type: histogram
  name: "foo"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					buckets:      prometheus.DefBuckets,
				},
			},
		},
		{
			testName: "Config with default histogram options overrides buckets",
			config: `---
defaults:
  buckets: [0.2, 2, 20, 200, 2000]
  histogram_options:
    buckets: [0.1, 1, 10, 100, 1000]
mappings:
- match: test.*.*
  observer_type: histogram
  name: "foo"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					buckets:      []float64{0.1, 1, 10, 100, 1000},
				},
			},
		},
		{
			testName: "Config that overrides default histogram configuration",
			config: `---
defaults:
  histogram_options:
    buckets: [0.2, 2, 20, 200, 2000]
mappings:
- match: test.*.*
  observer_type: histogram
  name: "foo"
  labels: {}
  histogram_options:
    buckets: [0.1, 1, 10, 100, 1000]
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					buckets:      []float64{0.1, 1, 10, 100, 1000},
				},
			},
		},
		{
			testName: "Config that overrides default histogram configuration and a default options mapping",
			config: `---
defaults:
  histogram_options:
    buckets: [0.2, 2, 20, 200]
mappings:
- match: test.*.*
  observer_type: histogram
  name: "foo"
  labels: {}
  histogram_options:
    buckets: [0.1, 1, 10, 100, 1000]
- match: test_default.*.*
  observer_type: histogram
  name: "foo_default"
  labels: {}
`,
			mappings: mappings{
				{
					statsdMetric: "test.*.*",
					name:         "foo",
					labels:       map[string]string{},
					buckets:      []float64{0.1, 1, 10, 100, 1000},
				},
				{
					statsdMetric: "test_default.*.*",
					name:         "foo_default",
					labels:       map[string]string{},
					buckets:      []float64{0.2, 2, 20, 200},
				},
			},
		},
		{
			testName: "Duplicate quantiles are bad",
			config: `---
mappings:
- match: test.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  quantiles:
    - quantile: 0.42
      error: 0.04
  summary_options:
    quantiles:
      - quantile: 0.42
        error: 0.04
  `,
			configBad: true,
		},
		{
			testName: "Config with good metric type",
			config: `---
mappings:
- match: test.*.*
  match_metric_type: counter
  name: "foo"
  labels: {}
    `,
		},
		{
			testName: "Config with good metric type observer",
			config: `---
mappings:
- match: test.*.*
  match_metric_type: observer
  name: "foo"
  labels: {}
    `,
		},
		{
			testName: "Config with good metric type timer",
			config: `---
mappings:
- match: test.*.*
  match_metric_type: timer
  name: "foo"
  labels: {}
    `,
		},
		{
			testName: "Config with bad metric type matcher",
			config: `---
mappings:
- match: test.*.*
  match_metric_type: wrong
  name: "foo"
  labels: {}
    `,
			configBad: true,
		},
		{
			testName: "Config with multiple explicit metric types",
			config: `---
mappings:
- match: test.foo.*
  name: "test_foo_sum"
  match_metric_type: counter
- match: test.foo.*
  name: "test_foo_current"
  match_metric_type: gauge
    `,
			mappings: mappings{
				{
					statsdMetric: "test.foo.test",
					name:         "test_foo_sum",
					metricType:   MetricTypeCounter,
				},
				{
					statsdMetric: "test.foo.test",
					name:         "test_foo_current",
					metricType:   MetricTypeGauge,
				},
			},
		},
		{
			testName: "Config with uncompilable regex",
			config: `---
mappings:
- match: "*\\.foo"
  match_type: regex
  name: "foo"
  labels: {}
    `,
			configBad: true,
		},
		{
			testName: "Config with non-matched metric",
			config: `---
mappings:
- match: foo.*.*
  observer_type: summary
  name: "foo"
  labels: {}
  `,
			mappings: mappings{
				{
					statsdMetric: "test.1.2",
					name:         "test_1_2",
					labels:       map[string]string{},
					notPresent:   true,
				},
			},
		},
		{
			testName: "Config with no name",
			config: `---
mappings:
- match: *\.foo
  match_type: regex
  labels:
    bar: "foo"
    `,
			configBad: true,
		},
		{
			testName: "Config with labels from glob",
			config: `---
mappings:
- match: p.*.*.c.*
  match_type: glob
  name: issue_256
  labels:
    one: $1
    two: $2
    three: $3
`,
			mappings: mappings{
				{
					statsdMetric: "p.one.two.c.three",
					name:         "issue_256",
					labels: map[string]string{
						"one":   "one",
						"two":   "two",
						"three": "three",
					},
				},
			},
		},
		{
			testName: "Example from the README",
			config: `
mappings:
- match: test.dispatcher.*.*.*
  name: "dispatcher_events_total"
  labels:
    processor: "$1"
    action: "$2"
    outcome: "$3"
    job: "test_dispatcher"
- match: "*.signup.*.*"
  name: "signup_events_total"
  labels:
    provider: "$2"
    outcome: "$3"
    job: "${1}_server"
`,
			mappings: mappings{
				{
					statsdMetric: "test.dispatcher.FooProcessor.send.success",
					name:         "dispatcher_events_total",
					labels: map[string]string{
						"processor": "FooProcessor",
						"action":    "send",
						"outcome":   "success",
						"job":       "test_dispatcher",
					},
				},
				{
					statsdMetric: "foo_product.signup.facebook.failure",
					name:         "signup_events_total",
					labels: map[string]string{
						"provider": "facebook",
						"outcome":  "failure",
						"job":      "foo_product_server",
					},
				},
				{
					statsdMetric: "test.web-server.foo.bar",
					name:         "test_web_server_foo_bar",
					labels:       map[string]string{},
				},
			},
		},
		{
			testName: "Config that drops all",
			config: `mappings:
- match: .
  match_type: regex
  name: "drop"
  action: drop`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "abc",
				},
			},
		},
		{
			testName: "Config that has a catch-all to drop all",
			config: `mappings:
- match: web.*
  name: "web"
  labels:
    site: "$1"
- match: .
  match_type: regex
  name: "drop"
  action: drop`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "web.localhost",
					name:         "web",
					labels: map[string]string{
						"site": "localhost",
					},
				},
			},
		},
		{
			testName: "Config that has a ttl",
			config: `mappings:
- match: web.*
  name: "web"
  ttl: 10s
  labels:
    site: "$1"`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "web.localhost",
					name:         "web",
					labels: map[string]string{
						"site": "localhost",
					},
					ttl: time.Second * 10,
				},
			},
		},
		{
			testName: "Config that has a default ttl",
			config: `defaults:
  ttl: 1m2s
mappings:
- match: web.*
  name: "web"
  labels:
    site: "$1"`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "web.localhost",
					name:         "web",
					labels: map[string]string{
						"site": "localhost",
					},
					ttl: time.Minute + time.Second*2,
				},
			},
		},
		{
			testName: "Config that override a default ttl",
			config: `defaults:
  ttl: 1m2s
mappings:
- match: web.*
  name: "web"
  ttl: 5s
  labels:
    site: "$1"`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "web.localhost",
					name:         "web",
					labels: map[string]string{
						"site": "localhost",
					},
					ttl: time.Second * 5,
				},
			},
		},
		{
			testName: "Config with 'scale' field",
			config: `mappings:
- match: grpc_server.*.*.latency_ms
  name: grpc_server_handling_seconds
  scale: 0.001
  labels:
    grpc_service: "$1"
    grpc_method: "$2"`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "grpc_server.Foo.Bar.latency_ms",
					name:         "grpc_server_handling_seconds",
					scale:        MaybeFloat64{Val: 0.001, Set: true},
					labels: map[string]string{
						"grpc_service": "Foo",
						"grpc_method":  "Bar",
					},
				},
			},
		},
		{
			testName: "Config with 'scale' using scientific notation",
			config: `mappings:
- match: grpc_server.*.*.latency_us
  name: grpc_server_handling_seconds
  scale: 1e-6
  labels:
    grpc_service: "$1"
    grpc_method: "$2"`,
			mappings: mappings{
				{
					statsdMetric: "test.a",
				},
				{
					statsdMetric: "grpc_server.Foo.Bar.latency_us",
					name:         "grpc_server_handling_seconds",
					scale:        MaybeFloat64{Val: 1e-6, Set: true},
					labels: map[string]string{
						"grpc_service": "Foo",
						"grpc_method":  "Bar",
					},
				},
			},
		},
	}

	mapper := MetricMapper{}
	cache, _ := lru.NewMetricMapperLRUCache(mapper.Registerer, 1000)
	mapper.UseCache(cache)

	for i, scenario := range scenarios {
		if scenario.testName == "" {
			t.Fatalf("Missing testName in scenario %+v", scenario)
		}
		t.Run(scenario.testName, func(t *testing.T) {
			err := mapper.InitFromYAMLString(scenario.config)
			if err != nil && !scenario.configBad {
				t.Fatalf("%d. Config load error: %s %s", i, scenario.config, err)
			}
			if err == nil && scenario.configBad {
				t.Fatalf("%d. Expected bad config, but loaded ok: %s", i, scenario.config)
			}

			for metric, mapping := range scenario.mappings {
				// exporter will call mapper.GetMapping with valid MetricType
				// so we also pass a sane MetricType in testing if it's not specified
				mapType := mapping.metricType
				if mapType == "" {
					mapType = MetricTypeCounter
				}
				m, labels, present := mapper.GetMapping(mapping.statsdMetric, mapType)
				if present && mapping.name != "" && m.Name != mapping.name {
					t.Fatalf("%d.%q: Expected name %v, got %v", i, metric, m.Name, mapping.name)
				}
				if mapping.notPresent && present {
					t.Fatalf("%d.%q: Expected metric to not be present", i, metric)
				}
				if len(labels) != len(mapping.labels) {
					t.Fatalf("%d.%q: Expected %d labels, got %d", i, metric, len(mapping.labels), len(labels))
				}
				for label, value := range labels {
					if mapping.labels[label] != value {
						t.Fatalf("%d.%q: Expected labels %v, got %v", i, metric, mapping, labels)
					}
				}
				if mapping.ttl > 0 && mapping.ttl != m.Ttl {
					t.Fatalf("%d.%q: Expected ttl of %s, got %s", i, metric, mapping.ttl.String(), m.Ttl.String())
				}
				if mapping.metricType != "" && mapType != m.MatchMetricType {
					t.Fatalf("%d.%q: Expected match metric of %s, got %s", i, metric, mapType, m.MatchMetricType)
				}

				if len(mapping.buckets) != 0 {
					if len(mapping.buckets) != len(m.HistogramOptions.Buckets) {
						t.Fatalf("%d.%q: Expected %d buckets, got %d", i, metric, len(mapping.buckets), len(m.HistogramOptions.Buckets))
					}
					for i, bucket := range mapping.buckets {
						if bucket != m.HistogramOptions.Buckets[i] {
							t.Fatalf("%d.%q: Expected bucket %v, got %v", i, metric, m.HistogramOptions.Buckets[i], bucket)
						}
					}
				}

				if len(mapping.quantiles) != 0 {
					if len(mapping.quantiles) != len(m.SummaryOptions.Quantiles) {
						t.Fatalf("%d.%q: Expected %d quantiles, got %d", i, metric, len(mapping.quantiles), len(m.SummaryOptions.Quantiles))
					}
					for i, quantile := range mapping.quantiles {
						if quantile.Quantile != m.SummaryOptions.Quantiles[i].Quantile {
							t.Fatalf("%d.%q: Expected quantile %v, got %v", i, metric, m.SummaryOptions.Quantiles[i].Quantile, quantile.Quantile)
						}
						if quantile.Error != m.SummaryOptions.Quantiles[i].Error {
							t.Fatalf("%d.%q: Expected Error margin %v, got %v", i, metric, m.SummaryOptions.Quantiles[i].Error, quantile.Error)
						}
					}
				}
				if mapping.maxAge != 0 && mapping.maxAge != m.SummaryOptions.MaxAge {
					t.Fatalf("%d.%q: Expected max age %v, got %v", i, metric, mapping.maxAge, m.SummaryOptions.MaxAge)
				}
				if mapping.ageBuckets != 0 && mapping.ageBuckets != m.SummaryOptions.AgeBuckets {
					t.Fatalf("%d.%q: Expected max age %v, got %v", i, metric, mapping.ageBuckets, m.SummaryOptions.AgeBuckets)
				}
				if mapping.bufCap != 0 && mapping.bufCap != m.SummaryOptions.BufCap {
					t.Fatalf("%d.%q: Expected max age %v, got %v", i, metric, mapping.bufCap, m.SummaryOptions.BufCap)
				}
				if present && mapping.scale != m.Scale {
					t.Fatalf("%d.%q: Expected scale %v, got %v", i, metric, mapping.scale, m.Scale)
				}
			}
		})
	}
}

func TestAction(t *testing.T) {
	scenarios := []struct {
		testName       string
		config         string
		configBad      bool
		expectedAction ActionType
	}{
		{
			testName: "no action set",
			config: `---
mappings:
- match: test.*.*
  name: "foo"
`,
			configBad:      false,
			expectedAction: ActionTypeMap,
		},
		{
			testName: "map action set",
			config: `---
mappings:
- match: test.*.*
  name: "foo"
  action: map
`,
			configBad:      false,
			expectedAction: ActionTypeMap,
		},
		{
			testName: "drop action set",
			config: `---
mappings:
- match: test.*.*
  name: "foo"
  action: drop
`,
			configBad:      false,
			expectedAction: ActionTypeDrop,
		},
		{
			testName: "invalid action set",
			config: `---
mappings:
- match: test.*.*
  name: "foo"
  action: xyz
`,
			configBad:      true,
			expectedAction: ActionTypeDrop,
		},
		{
			testName: "valid yaml example",
			config: `---
mappings:
- match: "test\\.(\\w+)\\.(\\w+)\\.counter"
  match_type: regex
  name: "${2}_total"
  labels:
    provider: "$1"
`,
			configBad:      false,
			expectedAction: ActionTypeMap,
		},
		{
			testName: "invalid yaml example",
			config: `---
mappings:
- match: "test\.(\w+)\.(\w+)\.counter"
  match_type: regex
  name: "${2}_total"
  labels:
    provider: "$1"
`,
			configBad: true,
		},
	}

	for i, scenario := range scenarios {
		if scenario.testName == "" {
			t.Fatalf("Missing testName in scenario %+v", scenario)
		}
		t.Run(scenario.testName, func(t *testing.T) {
			mapper := MetricMapper{}
			err := mapper.InitFromYAMLString(scenario.config)
			if err != nil && !scenario.configBad {
				t.Fatalf("%d. Config load error: %s %s", i, scenario.config, err)
			}
			if err == nil && scenario.configBad {
				t.Fatalf("%d. Expected bad config, but loaded ok: %s", i, scenario.config)
			}

			if !scenario.configBad {
				a := mapper.Mappings[0].Action
				if scenario.expectedAction != a {
					t.Fatalf("%d: Expected action %v, got %v", i, scenario.expectedAction, a)
				}
			}
		})
	}
}

// Test for https://github.com/prometheus/statsd_exporter/issues/273
// Corrupt cache for multiple names matching in fsm
func TestMultipleMatches(t *testing.T) {
	config := `---
mappings:
- match: aa.bb.*.*
  name: "aa_bb_${1}_total"
  labels:
    app: "$2"
`

	names := map[string]string{
		"aa.bb.aa.myapp": "aa_bb_aa_total",
		"aa.bb.bb.myapp": "aa_bb_bb_total",
		"aa.bb.cc.myapp": "aa_bb_cc_total",
		"aa.bb.dd.myapp": "aa_bb_dd_total",
	}

	scenarios := []string{"none", "lru"}

	for i, scenario := range scenarios {
		mapper := newTestMapperWithCache(scenario, 1000)
		err := mapper.InitFromYAMLString(config)
		if err != nil {
			t.Fatalf("config load error: %s ", err)
		}

		// run multiple times to ensure cache works as expected
		for j := 0; j < 10; j++ {
			for name, expected := range names {
				m, _, ok := mapper.GetMapping(name, MetricTypeCounter)
				if !ok {
					t.Fatalf("%d:%d Did not find match for %s", i, j, name)
				}
				if m.Name != expected {
					t.Fatalf("%d:%d Expected name %s, got %s", i, j, expected, m.Name)
				}
			}
		}
	}
}
